package com.hubspot.jinjava.el; import static com.hubspot.jinjava.util.Logging.ENGINE_LOG; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import javax.el.ArrayELResolver; import javax.el.CompositeELResolver; import javax.el.ELContext; import javax.el.ELResolver; import javax.el.MapELResolver; import javax.el.PropertyNotFoundException; import javax.el.ResourceBundleELResolver; import org.apache.commons.lang3.LocaleUtils; import org.apache.commons.lang3.StringUtils; import com.google.common.collect.ImmutableMap; import com.hubspot.jinjava.el.ext.AbstractCallableMethod; import com.hubspot.jinjava.el.ext.ExtendedParser; import com.hubspot.jinjava.el.ext.JinjavaBeanELResolver; import com.hubspot.jinjava.el.ext.JinjavaListELResolver; import com.hubspot.jinjava.el.ext.NamedParameter; import com.hubspot.jinjava.interpret.DisabledException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory; import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.collections.PyList; import com.hubspot.jinjava.objects.collections.PyMap; import com.hubspot.jinjava.objects.date.FormattedDate; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.objects.date.StrftimeFormatter; import de.odysseus.el.util.SimpleResolver; public class JinjavaInterpreterResolver extends SimpleResolver { private static final ELResolver DEFAULT_RESOLVER_READ_ONLY = new CompositeELResolver() { { add(new ArrayELResolver(true)); add(new JinjavaListELResolver(true)); add(new MapELResolver(true)); add(new ResourceBundleELResolver()); add(new JinjavaBeanELResolver(true)); } }; private static final ELResolver DEFAULT_RESOLVER_READ_WRITE = new CompositeELResolver() { { add(new ArrayELResolver(false)); add(new JinjavaListELResolver(false)); add(new MapELResolver(false)); add(new ResourceBundleELResolver()); add(new JinjavaBeanELResolver(false)); } }; private final JinjavaInterpreter interpreter; public JinjavaInterpreterResolver(JinjavaInterpreter interpreter) { super(interpreter.getConfig().isReadOnlyResolver() ? DEFAULT_RESOLVER_READ_ONLY : DEFAULT_RESOLVER_READ_WRITE); this.interpreter = interpreter; } @Override public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params) { try { Object methodProperty = getValue(context, base, method, false); if (methodProperty instanceof AbstractCallableMethod) { context.setPropertyResolved(true); return ((AbstractCallableMethod) methodProperty).evaluate(params); } } catch (IllegalArgumentException e) { // failed to access property, continue with method calls } return super.invoke(context, base, method, paramTypes, generateMethodParams(method, params)); } /** * {@inheritDoc} * * If the base object is null, the property will be looked up in the context. */ @Override public Object getValue(ELContext context, Object base, Object property) { return getValue(context, base, property, true); } /* * We transform the AST parameters to something meaningful to Jinjava. * * Functions, expressions and tags will receive the parameters as they are, but filters * have a different signature to what they have in the AST to support named parameters, so * this method transforms their arguments to be the following: * * (Left Value, JinjavaInterpreter, Positional Arguments, Named Arguments) */ private Object[] generateMethodParams(Object method, Object[] astParams) { if (!"filter".equals(method)) { return astParams; // We only change the signature method for filters } List<Object> args = new ArrayList<>(); Map<String, Object> kwargs = new LinkedHashMap<>(); // 2 -> Ignore the Left Value (0) and the JinjavaInterpreter (1) for (Object param: Arrays.asList(astParams).subList(2, astParams.length)) { if (param instanceof NamedParameter) { NamedParameter namedParameter = (NamedParameter) param; kwargs.put(namedParameter.getName(), namedParameter.getValue()); } else { args.add(param); } } return new Object[] {astParams[0], astParams[1], args.toArray(), kwargs}; } private Object getValue(ELContext context, Object base, Object property, boolean errOnUnknownProp) { String propertyName = Objects.toString(property, ""); Object value = null; interpreter.getContext().addResolvedValue(propertyName); ErrorItem item = ErrorItem.PROPERTY; try { if (ExtendedParser.INTERPRETER.equals(property)) { value = interpreter; } else if (propertyName.startsWith(ExtendedParser.FILTER_PREFIX)) { item = ErrorItem.FILTER; value = interpreter.getContext().getFilter(StringUtils.substringAfter(propertyName, ExtendedParser.FILTER_PREFIX)); } else if (propertyName.startsWith(ExtendedParser.EXPTEST_PREFIX)) { item = ErrorItem.EXPRESSION_TEST; value = interpreter.getContext().getExpTest(StringUtils.substringAfter(propertyName, ExtendedParser.EXPTEST_PREFIX)); } else { if (base == null) { // Look up property in context. value = interpreter.retraceVariable((String) property, interpreter.getLineNumber()); } else { // Get property of base object. try { if (base instanceof Optional) { Optional<?> optBase = (Optional<?>) base; if (!optBase.isPresent()) { return null; } base = optBase.get(); } value = super.getValue(context, base, propertyName); if (value instanceof Optional) { Optional<?> optValue = (Optional<?>) value; if (!optValue.isPresent()) { return null; } value = optValue.get(); } } catch (PropertyNotFoundException e) { if (errOnUnknownProp) { interpreter.addError(TemplateError.fromUnknownProperty(base, propertyName, interpreter.getLineNumber())); } } } } } catch (DisabledException e) { interpreter.addError(new TemplateError(ErrorType.FATAL, ErrorReason.DISABLED, item, e.getMessage(), propertyName, interpreter.getLineNumber(), e)); } context.setPropertyResolved(true); return wrap(value); } @SuppressWarnings("unchecked") Object wrap(Object value) { if (value == null) { return null; } if (value instanceof PyWrapper) { return value; } if (List.class.isAssignableFrom(value.getClass())) { return new PyList((List<Object>) value); } if (Map.class.isAssignableFrom(value.getClass())) { // FIXME: ensure keys are actually strings, if not, convert them return new PyMap((Map<String, Object>) value); } if (Date.class.isAssignableFrom(value.getClass())) { return new PyishDate(localizeDateTime(interpreter, ZonedDateTime.ofInstant(Instant.ofEpochMilli(((Date) value).getTime()), ZoneOffset.UTC))); } if (ZonedDateTime.class.isAssignableFrom(value.getClass())) { return new PyishDate(localizeDateTime(interpreter, (ZonedDateTime) value)); } if (FormattedDate.class.isAssignableFrom(value.getClass())) { return formattedDateToString(interpreter, (FormattedDate) value); } return value; } private static ZonedDateTime localizeDateTime(JinjavaInterpreter interpreter, ZonedDateTime dt) { ENGINE_LOG.debug("Using timezone: {} to localize datetime: {}", interpreter.getConfig().getTimeZone(), dt); return dt.withZoneSameInstant(interpreter.getConfig().getTimeZone()); } private static String formattedDateToString(JinjavaInterpreter interpreter, FormattedDate d) { DateTimeFormatter formatter = getFormatter(interpreter, d).withLocale(getLocale(interpreter, d)); return formatter.format(localizeDateTime(interpreter, d.getDate())); } private static DateTimeFormatter getFormatter(JinjavaInterpreter interpreter, FormattedDate d) { if (!StringUtils.isBlank(d.getFormat())) { try { return StrftimeFormatter.formatter(d.getFormat(), interpreter.getConfig().getLocale()); } catch (IllegalArgumentException e) { interpreter.addError(new TemplateError(ErrorType.WARNING, ErrorReason.SYNTAX_ERROR, ErrorItem.OTHER, e.getMessage(), null, interpreter.getLineNumber(), null, BasicTemplateErrorCategory.UNKNOWN_DATE, ImmutableMap.of("date", d.getDate().toString(), "exception", e.getMessage(), "lineNumber", String.valueOf(interpreter.getLineNumber())))); } } return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); } private static Locale getLocale(JinjavaInterpreter interpreter, FormattedDate d) { if (!StringUtils.isBlank(d.getLanguage())) { try { return LocaleUtils.toLocale(d.getLanguage()); } catch (IllegalArgumentException e) { interpreter.addError(new TemplateError(ErrorType.WARNING, ErrorReason.SYNTAX_ERROR, ErrorItem.OTHER, e.getMessage(), null, interpreter.getLineNumber(), null, BasicTemplateErrorCategory.UNKNOWN_LOCALE, ImmutableMap.of("date", d.getDate().toString(), "exception", e.getMessage(), "lineNumber", String.valueOf(interpreter.getLineNumber())))); } } return Locale.US; } }